@file:Suppress("unused", "LocalVariableName")

package dev.entao.core

import java.io.IOException
import java.io.OutputStreamWriter
import java.io.PrintWriter
import java.io.UnsupportedEncodingException
import java.text.MessageFormat
import java.util.*
import javax.servlet.*
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import javax.servlet.http.HttpServletResponseWrapper

open class HttpFilter : Filter {
    lateinit var filterConfig: FilterConfig
        private set
    val servletContext: ServletContext get() = filterConfig.servletContext
    var stateFilter: Int = SF_NONE
        private set
    val isInited: Boolean get() = stateFilter == SF_INITED

    val filterName: String
        get() {
            checkInit()
            return filterConfig.filterName ?: ""
        }
    val initParameterNames: List<String>
        get() {
            checkInit()
            return filterConfig.initParameterNames.toList()
        }

    open fun onInit() {

    }

    override fun init(filterConfig: FilterConfig) {
        this.filterConfig = filterConfig
        stateFilter = SF_INITED
        onInit()
    }

    override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
        if (request is HttpServletRequest && response is HttpServletResponse) {
            request.characterEncoding = "UTF-8"
            response.characterEncoding = "UTF-8"
            service(request, response, chain)
        } else {
            chain.doFilter(request, response)
        }
    }

    open fun doGet(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
    }


    open fun doPost(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {

    }

    open fun doPut(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
    }

    open fun doDelete(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
    }

    open fun doHead(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
        val responseNone = NoBodyResponse(response)
        doGet(request, responseNone, chain)
        responseNone.setContentLength()
    }

    open fun allowMethods(request: HttpServletRequest): Set<String> {
        return setOf(METHOD_GET, METHOD_POST, METHOD_PUT, METHOD_DELETE)
    }

    fun allowCross(request: HttpServletRequest, response: HttpServletResponse) {
        val origin = request.header("Origin")
        if (origin != null) {
            response.setHeader("Access-Control-Allow-Origin", origin)
            response.setHeader("Access-Control-Allow-Credentials", "true")
            response.setHeader("Access-Control-Allow-Methods", "GET,POST,HEAD,OPTIONS")
            response.setHeader(
                "Access-Control-Allow-Headers",
                "Origin,Accept,Content-Type,Content-Length,X-Requested-With,Key,Token,Authorization"
            )

        }
    }

    open fun doOptions(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
        val ls = ArrayList<String>()
        val set = allowMethods(request)
        if (METHOD_GET in set) {
            ls += METHOD_GET
            ls += METHOD_HEAD
        }
        if (METHOD_POST in set) ls += METHOD_POST
        if (METHOD_PUT in set) ls += METHOD_PUT
        if (METHOD_DELETE in set) ls += METHOD_DELETE

        ls += METHOD_OPTIONS
        ls += METHOD_TRACE

        response.setHeader("Allow", ls.joinToString(","))
    }

    open fun doTrace(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
        val responseLength: Int
        val CRLF = "\r\n"
        val buffer = StringBuilder("TRACE ").append(request.requestURI).append(" ").append(request.protocol)
        val reqHeaderEnum = request.headerNames
        while (reqHeaderEnum.hasMoreElements()) {
            val headerName = reqHeaderEnum.nextElement()
            buffer.append(CRLF).append(headerName).append(": ").append(request.getHeader(headerName))
        }
        buffer.append(CRLF)
        responseLength = buffer.length
        response.contentType = "message/http"
        response.setContentLength(responseLength)
        val out = response.outputStream
        out.print(buffer.toString())
    }

    @Throws(ServletException::class, IOException::class)
    protected open fun service(req: HttpServletRequest, resp: HttpServletResponse, chain: FilterChain) {
        val method = req.method.uppercase()
        if (method == METHOD_GET) {
            val lastModified: Long = getLastModified(req)
            if (lastModified == -1L) {
                doGet(req, resp, chain)
            } else {
                val ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE)
                if (ifModifiedSince < lastModified) {
                    maybeSetLastModified(resp, lastModified)
                    doGet(req, resp, chain)
                } else {
                    resp.status = HttpServletResponse.SC_NOT_MODIFIED
                }
            }
        } else if (method == METHOD_HEAD) {
            val lastModified: Long = getLastModified(req)
            maybeSetLastModified(resp, lastModified)
            doHead(req, resp, chain)
        } else if (method == METHOD_POST) {
            doPost(req, resp, chain)
        } else if (method == METHOD_PUT) {
            doPut(req, resp, chain)
        } else if (method == METHOD_DELETE) {
            doDelete(req, resp, chain)
        } else if (method == METHOD_OPTIONS) {
            doOptions(req, resp, chain)
        } else if (method == METHOD_TRACE) {
            doTrace(req, resp, chain)
        } else {
            resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, "Method NOT implemented")
        }
    }

    protected open fun getLastModified(req: HttpServletRequest): Long {
        return -1
    }

    private fun maybeSetLastModified(resp: HttpServletResponse, lastModified: Long) {
        if (resp.containsHeader(HEADER_LASTMOD)) return
        if (lastModified >= 0) resp.setDateHeader(HEADER_LASTMOD, lastModified)
    }

    open fun onDestroy() {

    }

    override fun destroy() {
        onDestroy()
        stateFilter = SF_DESTROYED
    }

    fun getInitParameter(name: String): String? {
        checkInit()
        return filterConfig.getInitParameter(name)
    }

    private fun checkInit() {
        if (!isInited) error("HttpFilter NOT inited")
    }


    companion object {
        const val SF_NONE = 0
        const val SF_INITED = 1
        const val SF_DESTROYED = 2

        const val METHOD_DELETE = "DELETE"
        const val METHOD_HEAD = "HEAD"
        const val METHOD_GET = "GET"
        const val METHOD_OPTIONS = "OPTIONS"
        const val METHOD_POST = "POST"
        const val METHOD_PUT = "PUT"
        const val METHOD_TRACE = "TRACE"

        const val HEADER_IFMODSINCE = "If-Modified-Since"
        const val HEADER_LASTMOD = "Last-Modified"

        private val lStrings = ResourceBundle.getBundle("javax.servlet.http.LocalStrings")
    }
}

private class NoBodyResponse(r: HttpServletResponse) : HttpServletResponseWrapper(r) {
    private val noBody: NoBodyOutputStream = NoBodyOutputStream()
    private var printWriter: PrintWriter? = null
    private var didSetContentLength = false
    private var usingOutputStream = false

    // file private
    fun setContentLength() {
        if (!didSetContentLength) {
            printWriter?.flush()
            setContentLength(noBody.contentLength)
        }
    }

    override fun setContentLength(len: Int) {
        super.setContentLength(len)
        didSetContentLength = true
    }

    override fun setContentLengthLong(len: Long) {
        super.setContentLengthLong(len)
        didSetContentLength = true
    }

    override fun setHeader(name: String, value: String) {
        super.setHeader(name, value)
        checkHeader(name)
    }

    override fun addHeader(name: String, value: String) {
        super.addHeader(name, value)
        checkHeader(name)
    }

    override fun setIntHeader(name: String, value: Int) {
        super.setIntHeader(name, value)
        checkHeader(name)
    }

    override fun addIntHeader(name: String, value: Int) {
        super.addIntHeader(name, value)
        checkHeader(name)
    }

    private fun checkHeader(name: String) {
        if ("content-length".equals(name, ignoreCase = true)) {
            didSetContentLength = true
        }
    }

    @Throws(IOException::class)
    override fun getOutputStream(): ServletOutputStream {
        check(printWriter == null) { lStrings.getString("err.ise.getOutputStream") }
        usingOutputStream = true
        return noBody
    }

    @Throws(UnsupportedEncodingException::class)
    override fun getWriter(): PrintWriter {
        check(!usingOutputStream) { lStrings.getString("err.ise.getWriter") }
        if (printWriter == null) {
            val w = OutputStreamWriter(noBody, characterEncoding)
            printWriter = PrintWriter(w)
        }
        return printWriter!!
    }

    companion object {
        private val lStrings = ResourceBundle.getBundle("javax.servlet.http.LocalStrings")
    }
}


// file private
private class NoBodyOutputStream : ServletOutputStream() {
    var contentLength = 0
        private set

    override fun write(b: Int) {
        contentLength++
    }

    @Throws(IOException::class)
    override fun write(buf: ByteArray, offset: Int, len: Int) {
        if (offset < 0 || len < 0 || offset + len > buf.size) {
            var msg = lStrings.getString("err.io.indexOutOfBounds")
            val msgArgs = arrayOfNulls<Any>(3)
            msgArgs[0] = Integer.valueOf(offset)
            msgArgs[1] = Integer.valueOf(len)
            msgArgs[2] = Integer.valueOf(buf.size)
            msg = MessageFormat.format(msg, *msgArgs)
            throw IndexOutOfBoundsException(msg)
        }
        contentLength += len
    }

    override fun isReady(): Boolean {
        return false
    }

    override fun setWriteListener(writeListener: WriteListener) {}

    companion object {
        private const val LSTRING_FILE = "javax.servlet.http.LocalStrings"
        private val lStrings = ResourceBundle.getBundle(LSTRING_FILE)
    }
}
